using System;
using System.Collections;
//using ArrayIterator = Velocity4Net.Util.ArrayIterator;
//using EnumerationIterator = Velocity4Net.Util.EnumerationIterator;
using Token = Velocity4Net.Runtime.Parse.Token;
using MethodInvocationException = Velocity4Net.Errors.MethodInvocationException;
using ParseErrorException = Velocity4Net.Errors.ParseErrorException;
using ResourceNotFoundException = Velocity4Net.Errors.ResourceNotFoundException;
using Introspector = Velocity4Net.Util.Introspection.Introspector;
using IntrospectionCacheData = Velocity4Net.Util.Introspection.IntrospectionCacheData;
using Velocity4Net.Ctx;
using Velocity4Net.Runtime.Parse.AST;
using Velocity4Net.Util.Introspection;
using System.IO;
using Velocity4Net.Errors;
using Velocity4Net.Util;
using Velocity4Net.Runtime.Logging;

namespace Velocity4Net.Runtime.Directives
{
    /**
     * Foreach directive used for moving through arrays,
     * or objects that provide an Iterator.
     *
     * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
     * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
     * @author Daniel Rall
     * @version $Id: Foreach.java 945927 2010-05-18 22:21:41Z nbubna $
     */
    public class Foreach : Directive
    {
        /**
         * A special context to use when the foreach iterator returns a null.  This
         * is required since the standard context may not support nulls.
         * All puts and gets are passed through, except for the foreach iterator key.
         * @since 1.5
         */
        internal class NullHolderContext : ChainedInternalContextAdapter
        {
            private String loopVariableKey = "";
            private bool active = true;

            /**
             * Create the context as a wrapper to be used within the foreach
             * @param key the reference used in the foreach
             * @param context the parent context
             */
            internal NullHolderContext(String key, InternalContextAdapter context)
                : base(context)
            {
                if (key != null)
                    loopVariableKey = key;
            }

            /**
             * Get an object from the context, or null if the key is equal to the loop variable
             * @see org.apache.velocity.context.InternalContextAdapter#get(java.lang.String)
             * @exception MethodInvocationException passes on potential exception from reference method call
             */
            public Object Get(String key)
            {
                return (active && loopVariableKey.Equals(key))
                    ? null
                    : base.Get(key);
            }

            /**
             * @see org.apache.velocity.context.InternalContextAdapter#put(java.lang.String key, java.lang.Object value)
             */
            public Object Put(String key, Object value)
            {
                if (loopVariableKey.Equals(key) && (value == null))
                {
                    active = true;
                }

                return base.Put(key, value);
            }

            /**
             * Allows callers to explicitly put objects in the local context.
             * Objects added to the context through this method always end up
             * in the top-level context of possible wrapped contexts.
             *
             * @param key name of item to set.
             * @param value object to set to key.
             * @see org.apache.velocity.context.InternalWrapperContext#localPut(String, Object)
             */
            public Object localPut(String key, Object value)
            {
                return Put(key, value);
            }

            ///<summary>
            /// Remove an object from the context
            /// <see cref="Velocity4Net.Ctx.InternalContextAdapter"/>
            ///</summary>
            public Object Remove(Object key)
            {
                if (loopVariableKey.Equals(key))
                {
                    active = false;
                }
                return base.Remove(key);
            }
        }

        /**
         * Return name of this directive.
         * @return The name of this directive.
         */
        public override String Name
        {
            get
            {
                return "foreach";
            }
            set { }
        }

        /**
         * Return type of this directive.
         * @return The type of this directive.
         */
        public override int Type
        {
            get
            {
                return 1;
            }
        }

        /**
         * The name of the variable to use when placing
         * the counter value into the context. Right
         * now the default is $velocityCount.
         */
        private String counterName;

        /**
         * The name of the variable to use when placing
         * iterator hasNext() value into the context.Right
         * now the defailt is $velocityHasNext
         */
        private String hasNextName;

        /**
         * What value to start the loop counter at.
         */
        private int counterInitialValue;

        /**
         * The maximum number of times we're allowed to loop.
         */
        private int maxNbrLoops;

        /**
         * Whether or not to throw an Exception if the iterator is null.
         */
        private bool skipInvalidIterator;

        /**
         * The reference name used to access each
         * of the elements in the list object. It
         * is the $item in the following:
         *
         * #foreach ($item in $list)
         *
         * This can be used class wide because
         * it is immutable.
         */
        private String elementKey;

        // track if we've done the deprecation warning thing already
        private bool warned = false;

        /**
         *  immutable, so create in init
         */
        protected Info uberInfo;

        /**
         *  simple init - init the tree and get the elementKey from
         *  the AST
         * @param rs
         * @param context
         * @param node
         * @throws TemplateInitException
         */
        public override void Init(RuntimeServices rs, InternalContextAdapter context, INode node)
        {
            base.Init(rs, context, node);

            // handle deprecated config settings
            counterName = rsvc.GetString(RuntimeConstants.COUNTER_NAME);
            hasNextName = rsvc.GetString(RuntimeConstants.HAS_NEXT_NAME);
            counterInitialValue = rsvc.GetInt(RuntimeConstants.COUNTER_INITIAL_VALUE);
            // only warn once per instance...
            if (!warned && rsvc.getLog().isWarnEnabled())
            {
                warned = true;
                // ...and only if they customize these settings
                if (!"velocityCount".Equals(counterName))
                {
                    rsvc.getLog().warn("The " + RuntimeConstants.COUNTER_NAME +
                        " property has been deprecated. It will be removed" +
                        " (along with $velocityCount itself) in Velocity 2.0. " +
                        " Instead, please use $foreach.count to access" +
                        " the loop counter.");
                }
                if (!"velocityHasNext".Equals(hasNextName))
                {
                    rsvc.getLog().warn("The " + RuntimeConstants.HAS_NEXT_NAME +
                        " property has been deprecated. It will be removed" +
                        " (along with $velocityHasNext itself ) in Velocity 2.0. " +
                        " Instead, please use $foreach.hasNext to access" +
                        " this value from now on.");
                }
                if (counterInitialValue != 1)
                {
                    rsvc.getLog().warn("The " + RuntimeConstants.COUNTER_INITIAL_VALUE +
                        " property has been deprecated. It will be removed" +
                        " (along with $velocityCount itself) in Velocity 2.0. " +
                        " Instead, please use $foreach.index to access" +
                        " the 0-based loop index and $foreach.count" +
                        " to access the 1-based loop counter.");
                }
            }

            maxNbrLoops = rsvc.GetInt(RuntimeConstants.MAX_NUMBER_LOOPS,
                                      int.MaxValue);
            if (maxNbrLoops < 1)
            {
                maxNbrLoops = int.MaxValue;
            }
            skipInvalidIterator =
                rsvc.GetBoolean(RuntimeConstants.SKIP_INVALID_ITERATOR, true);

            if (rsvc.GetBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false))
            {
                // If we are in strict mode then the default for skipInvalidItarator
                // is true.  However, if the property is explicitly set, then honor the setting.
                skipInvalidIterator = rsvc.GetBoolean(RuntimeConstants.SKIP_INVALID_ITERATOR, false);
            }

            /*
             *  this is really the only thing we can do here as everything
             *  else is context sensitive
             */
            SimpleNode sn = (SimpleNode)node.jjtGetChild(0);

            if (sn is ASTReference)
            {
                elementKey = ((ASTReference)sn).RootString;
            }
            else
            {
                /*
                 * the default, error-prone way which we'll remove
                 *  TODO : remove if all goes well
                 */
                elementKey = sn.FirstToken.image.Substring(1);
            }

            /*
             * make an uberinfo - saves new's later on
             */

            uberInfo = new Info(this.TemplateName,
                    Line, Column);
        }

        /**
         * Extension hook to allow subclasses to control whether loop vars
         * are set locally or not. So, those in favor of VELOCITY-285, can
         * make that happen easily by overriding this and having it use
         * context.localPut(k,v). See VELOCITY-630 for more on this.
         */
        protected void put(InternalContextAdapter context, String key, Object value)
        {
            context.Put(key, value);
        }

        /**
         *  renders the #foreach() block
         * @param context
         * @param writer
         * @param node
         * @return True if the directive rendered successfully.
         * @throws IOException
         * @throws MethodInvocationException
         * @throws ResourceNotFoundException
         * @throws ParseErrorException
         */
        public override bool Render(InternalContextAdapter context,
                               TextWriter writer, INode node)
        {
            /*
             *  do our introspection to see what our collection is
             */
            INode child = node.jjtGetChild(2);
            Object listObject = child.GetValue(context);

            if (listObject == null)
                return false;

            IEnumerable i = null;

            try
            {
                i = rsvc.getUberspect().getIterator(listObject, uberInfo);
            }
            /**
             * pass through application level runtime exceptions
             */
            catch (System.ApplicationException e)
            {
                throw e;
            }
            catch (System.Exception ee)
            {
                String msg = "Error getting iterator for #foreach at " + uberInfo;
                rsvc.getLog().error(msg, ee);
                throw new VelocityException(msg, ee);
            }

            if (i == null)
            {
                if (skipInvalidIterator)
                {
                    return false;
                }
                else
                {
                    INode pnode = node.jjtGetChild(2);
                    String msg = "#foreach parameter " + pnode.Literal() + " at "
                        + Log.formatFileString(pnode)
                        + " is of type " + listObject.GetType().Name
                        + " and is either of wrong type or cannot be iterated.";
                    rsvc.getLog().error(msg);
                    throw new VelocityException(msg);
                }
            }

            int counter = counterInitialValue;
            bool maxNbrLoopsExceeded = false;

            /*
             *  save the element key if there is one, and the loop counter
             */
            Object o = context.Get(elementKey);
            Object savedCounter = context.Get(counterName);
            Object nextFlag = context.Get(hasNextName);

            /*
             * roll our own scope class instead of using preRender(ctx)'s
             */
            ForeachScope _foreach = null;
            if (ScopeProvided)
            {
                String name = ScopeName;
                _foreach = new ForeachScope(this, context.Get(name));
                context.Put(name, _foreach);
            }

            /*
             * Instantiate the null holder context if a null value
             * is returned by the foreach iterator.  Only one instance is
             * created - it's reused for every null value.
             */
            NullHolderContext nullHolderContext = null;
            IList objs = new ArrayList();
            foreach (Object value in i)
            {
                objs.Add(value);
            }
            foreach (Object value in objs)
            {
                if (!maxNbrLoopsExceeded)
                {
                    // TODO: JDK 1.5+ -> Integer.valueOf()
                    put(context, counterName, counter);
                    //put(context, hasNextName, i.hasNext());
                    put(context, elementKey, value);

                    if (ScopeProvided)
                    {
                        // update the scope control
                        _foreach.index++;
                        _foreach.hasNext = (objs.IndexOf(value) < objs.Count - 1);
                    }

                    try
                    {
                        /*
                         * If the value is null, use the special null holder context
                         */
                        if (value == null)
                        {
                            if (nullHolderContext == null)
                            {
                                // lazy instantiation
                                nullHolderContext = new NullHolderContext(elementKey, context);
                            }
                            node.jjtGetChild(3).Render(nullHolderContext, writer);
                        }
                        else
                        {
                            node.jjtGetChild(3).Render(context, writer);
                        }
                    }
                    catch (StopCommand stop)
                    {
                        if (stop.isFor(this))
                        {
                            break;
                        }
                        else
                        {
                            // clean up first
                            clean(context, o, savedCounter, nextFlag);
                            throw stop;
                        }
                    }

                    counter++;

                    // Determine whether we're allowed to continue looping.
                    // ASSUMPTION: counterInitialValue is not negative!
                    maxNbrLoopsExceeded = (counter - counterInitialValue) >= maxNbrLoops;
                }
            }
            clean(context, o, savedCounter, nextFlag);
            return true;
        }

        protected void clean(InternalContextAdapter context,
                             Object o, Object savedCounter, Object nextFlag)
        {
            /*
             *  restores element key if exists
             *  otherwise just removes
             */
            if (o != null)
            {
                context.Put(elementKey, o);
            }
            else
            {
                context.Remove(elementKey);
            }

            /*
             * restores the loop counter (if we were nested)
             * if we have one, else just removes
             */
            if (savedCounter != null)
            {
                context.Put(counterName, savedCounter);
            }
            else
            {
                context.Remove(counterName);
            }

            /*
             * restores the "hasNext" boolean flag if it exists
             */
            if (nextFlag != null)
            {
                context.Put(hasNextName, nextFlag);
            }
            else
            {
                context.Remove(hasNextName);
            }

            // clean up after the ForeachScope
            postRender(context);
        }
    }
}
